Micron Document
🎖️GitЯра🎖️

Commit df4f10c4d64a273688aa9dbe5284ac3737756310


Parents : f6587a1
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-05-18T12:29:27-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-05-18T17:29:27Z

fix(nav): remote admin nodenum + Nav3 consolidation and improvements (#5478)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Changes

23 files changed, 196 insertions(+), 249 deletions(-)


Diff

diff --git a/androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt b/androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt
index 46409b14eb..9177c7edb5 100644
--- a/androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt
+++ b/androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt
@@ -60,7 +60,7 @@ fun MainScreen() {
if (viewModel.currentDeviceAddressFlow.value.isNullOrSelectedNone()) {
TopLevelDestination.Connections.route
} else {
- NodesRoute.NodesGraph
+ NodesRoute.Nodes
}
val multiBackstack = rememberMultiBackstack(initialTab)
val backStack = multiBackstack.activeBackStack

diff --git a/androidApp/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt b/androidApp/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt
index 1fd4b39ce7..8f3bf2c71c 100644
--- a/androidApp/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt
+++ b/androidApp/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt
@@ -43,7 +43,7 @@ class NavigationAssemblyTest {
@Test
fun verifyNavigationGraphsAssembleWithoutCrashing() = runComposeUiTest {
setContent {
- val backStack = rememberNavBackStack(NodesRoute.NodesGraph)
+ val backStack = rememberNavBackStack(NodesRoute.Nodes)
entryProvider<NavKey> {
contactsGraph(backStack, emptyFlow())
nodesGraph(backStack = backStack, scrollToTopEvents = emptyFlow())

diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/DeepLinkRouter.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/DeepLinkRouter.kt
index 2351cd876c..f88dabc601 100644
--- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/DeepLinkRouter.kt
+++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/DeepLinkRouter.kt
@@ -60,7 +60,7 @@ object DeepLinkRouter {
"quickchat",
-> routeContacts(uri, pathSegments)
- "connections" -> listOf(ConnectionsRoute.ConnectionsGraph)
+ "connections" -> listOf(ConnectionsRoute.Connections)
"map" -> routeMap(uri, pathSegments)
@@ -68,7 +68,7 @@ object DeepLinkRouter {
"settings" -> routeSettings(pathSegments)
- "channels" -> listOf(ChannelsRoute.ChannelsGraph)
+ "channels" -> listOf(ChannelsRoute.Channels)
"firmware" -> routeFirmware(pathSegments)
@@ -86,27 +86,24 @@ object DeepLinkRouter {
return when (firstSegment) {
"share" -> {
val message = uri.getQueryParameter("message") ?: ""
- listOf(ContactsRoute.ContactsGraph, ContactsRoute.Share(message))
+ listOf(ContactsRoute.Contacts, ContactsRoute.Share(message))
}
"quickchat" -> {
- listOf(ContactsRoute.ContactsGraph, ContactsRoute.QuickChat)
+ listOf(ContactsRoute.Contacts, ContactsRoute.QuickChat)
}
"messages" -> {
val contactKey = if (segments.size > 1) segments[1] else uri.getQueryParameter("contactKey") ?: ""
val message = uri.getQueryParameter("message") ?: ""
if (contactKey.isNotBlank()) {
- listOf(
- ContactsRoute.ContactsGraph,
- ContactsRoute.Messages(contactKey = contactKey, message = message),
- )
+ listOf(ContactsRoute.Contacts, ContactsRoute.Messages(contactKey = contactKey, message = message))
} else {
- listOf(ContactsRoute.ContactsGraph)
+ listOf(ContactsRoute.Contacts)
}
}
- else -> listOf(ContactsRoute.ContactsGraph)
+ else -> listOf(ContactsRoute.Contacts)
}
}
@@ -121,17 +118,17 @@ object DeepLinkRouter {
val destNum = destNumStr?.toIntOrNull()
return if (destNum == null) {
- listOf(NodesRoute.NodesGraph)
+ listOf(NodesRoute.Nodes)
} else if (segments.size > 2) {
val subRouteStr = segments[2].lowercase()
val detailRouteFn = nodeDetailSubRoutes[subRouteStr]
if (detailRouteFn != null) {
- listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetailGraph(destNum), detailRouteFn(destNum))
+ listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum), detailRouteFn(destNum))
} else {
- listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum))
+ listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum))
}
} else {
- listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum))
+ listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum))
}
}
@@ -153,14 +150,14 @@ object DeepLinkRouter {
}
if (subRouteStr == null) {
- return listOf(SettingsRoute.SettingsGraph(destNum))
+ return listOf(SettingsRoute.Settings(destNum))
}
val subRoute = settingsSubRoutes[subRouteStr]
return if (subRoute != null) {
- listOf(SettingsRoute.SettingsGraph(destNum), subRoute)
+ listOf(SettingsRoute.Settings(destNum), subRoute)
} else {
- listOf(SettingsRoute.SettingsGraph(destNum))
+ listOf(SettingsRoute.Settings(destNum))
}
}

diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/MultiBackstack.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/MultiBackstack.kt
index 067ee2ae7e..49f1f55a69 100644
--- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/MultiBackstack.kt
+++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/MultiBackstack.kt
@@ -17,20 +17,23 @@
package org.meshtastic.core.navigation
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberNavBackStack
/** Manages independent backstacks for multiple tabs. */
-class MultiBackstack(val startTab: NavKey) {
+class MultiBackstack(val startTab: NavKey, private val currentTabState: MutableState<NavKey>) {
var backStacks: Map<NavKey, NavBackStack<NavKey>> = emptyMap()
- var currentTabRoute: NavKey by mutableStateOf(TopLevelDestination.fromNavKey(startTab)?.route ?: startTab)
+ var currentTabRoute: NavKey by currentTabState
private set
val activeBackStack: NavBackStack<NavKey>
@@ -73,6 +76,19 @@ class MultiBackstack(val startTab: NavKey) {
}
}
+/** Saver that persists the active tab ordinal across process death. */
+private val CurrentTabSaver =
+ Saver<MutableState<NavKey>, Int>(
+ save = { state ->
+ TopLevelDestination.entries.indexOfFirst { it.route::class == state.value::class }.takeIf { it >= 0 }
+ },
+ restore = { ordinal ->
+ mutableStateOf(
+ TopLevelDestination.entries.getOrNull(ordinal)?.route ?: TopLevelDestination.Connections.route,
+ )
+ },
+ )
+
/** Remembers a [MultiBackstack] for managing independent tab navigation histories with Navigation 3. */
@Composable
fun rememberMultiBackstack(initialTab: NavKey = TopLevelDestination.Connections.route): MultiBackstack {
@@ -82,7 +98,12 @@ fun rememberMultiBackstack(initialTab: NavKey = TopLevelDestination.Connections.
key(dest.route) { stacks[dest.route] = rememberNavBackStack(MeshtasticNavSavedStateConfig, dest.route) }
}
- val multiBackstack = remember { MultiBackstack(initialTab) }
+ val currentTabState =
+ rememberSaveable(saver = CurrentTabSaver) {
+ mutableStateOf(TopLevelDestination.fromNavKey(initialTab)?.route ?: initialTab)
+ }
+
+ val multiBackstack = remember { MultiBackstack(initialTab, currentTabState) }
multiBackstack.backStacks = stacks
return multiBackstack

diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt
index d8682c40cc..a0df53eb3b 100644
--- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt
+++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt
@@ -27,23 +27,17 @@ interface Graph : Route
@Serializable
sealed interface ChannelsRoute : Route {
- @Serializable data object ChannelsGraph : ChannelsRoute, Graph
-
- @Serializable data object Channels : ChannelsRoute
+ @Serializable data object Channels : ChannelsRoute, Graph
}
@Serializable
sealed interface ConnectionsRoute : Route {
- @Serializable data object ConnectionsGraph : ConnectionsRoute, Graph
-
- @Serializable data object Connections : ConnectionsRoute
+ @Serializable data object Connections : ConnectionsRoute, Graph
}
@Serializable
sealed interface ContactsRoute : Route {
- @Serializable data object ContactsGraph : ContactsRoute, Graph
-
- @Serializable data object Contacts : ContactsRoute
+ @Serializable data object Contacts : ContactsRoute, Graph
@Serializable data class Messages(val contactKey: String, val message: String = "") : ContactsRoute
@@ -59,13 +53,7 @@ sealed interface MapRoute : Route {
@Serializable
sealed interface NodesRoute : Route {
- @Serializable data object NodesGraph : NodesRoute, Graph
-
- @Serializable data object Nodes : NodesRoute
-
- @Serializable data class NodeDetailGraph(val destNum: Int? = null) :
- NodesRoute,
- Graph
+ @Serializable data object Nodes : NodesRoute, Graph
@Serializable data class NodeDetail(val destNum: Int? = null) : NodesRoute
}
@@ -96,12 +84,10 @@ sealed interface NodeDetailRoute : Route {
@Serializable
sealed interface SettingsRoute : Route {
- @Serializable data class SettingsGraph(val destNum: Int? = null) :
+ @Serializable data class Settings(val destNum: Int? = null) :
SettingsRoute,
Graph
- @Serializable data class Settings(val destNum: Int? = null) : SettingsRoute
-
@Serializable data object DeviceConfiguration : SettingsRoute
@Serializable data object ModuleConfiguration : SettingsRoute

diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt
index 8c8c4eea76..635ecfb7f4 100644
--- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt
+++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt
@@ -32,11 +32,11 @@ import org.meshtastic.core.resources.nodes
* and Desktop navigation shells.
*/
enum class TopLevelDestination(val label: StringResource, val route: Route) {
- Conversations(Res.string.conversations, ContactsRoute.ContactsGraph),
- Nodes(Res.string.nodes, NodesRoute.NodesGraph),
+ Conversations(Res.string.conversations, ContactsRoute.Contacts),
+ Nodes(Res.string.nodes, NodesRoute.Nodes),
Map(Res.string.map, MapRoute.Map()),
- Settings(Res.string.bottom_nav_settings, SettingsRoute.SettingsGraph()),
- Connections(Res.string.connections, ConnectionsRoute.ConnectionsGraph),
+ Settings(Res.string.bottom_nav_settings, SettingsRoute.Settings()),
+ Connections(Res.string.connections, ConnectionsRoute.Connections),
;
companion object {

diff --git a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt
index 04bda74728..c6fc642bd2 100644
--- a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt
+++ b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt
@@ -47,25 +47,25 @@ class DeepLinkRouterTest {
@Test
fun `share with message`() {
assertEquals(
- listOf(ContactsRoute.ContactsGraph, ContactsRoute.Share("hello world")),
+ listOf(ContactsRoute.Contacts, ContactsRoute.Share("hello world")),
route("/share?message=hello%20world"),
)
}
@Test
fun `share without message defaults to empty string`() {
- assertEquals(listOf(ContactsRoute.ContactsGraph, ContactsRoute.Share("")), route("/share"))
+ assertEquals(listOf(ContactsRoute.Contacts, ContactsRoute.Share("")), route("/share"))
}
@Test
fun `quickchat routes to QuickChat`() {
- assertEquals(listOf(ContactsRoute.ContactsGraph, ContactsRoute.QuickChat), route("/quickchat"))
+ assertEquals(listOf(ContactsRoute.Contacts, ContactsRoute.QuickChat), route("/quickchat"))
}
@Test
fun `messages with contactKey path segment`() {
assertEquals(
- listOf(ContactsRoute.ContactsGraph, ContactsRoute.Messages(contactKey = "abc123", message = "")),
+ listOf(ContactsRoute.Contacts, ContactsRoute.Messages(contactKey = "abc123", message = "")),
route("/messages/abc123"),
)
}
@@ -73,7 +73,7 @@ class DeepLinkRouterTest {
@Test
fun `messages with contactKey query param`() {
assertEquals(
- listOf(ContactsRoute.ContactsGraph, ContactsRoute.Messages(contactKey = "contact1", message = "")),
+ listOf(ContactsRoute.Contacts, ContactsRoute.Messages(contactKey = "contact1", message = "")),
route("/messages?contactKey=contact1"),
)
}
@@ -81,14 +81,14 @@ class DeepLinkRouterTest {
@Test
fun `messages with contactKey and message`() {
assertEquals(
- listOf(ContactsRoute.ContactsGraph, ContactsRoute.Messages(contactKey = "contact1", message = "hi")),
+ listOf(ContactsRoute.Contacts, ContactsRoute.Messages(contactKey = "contact1", message = "hi")),
route("/messages/contact1?message=hi"),
)
}
@Test
fun `messages without contactKey returns graph only`() {
- assertEquals(listOf(ContactsRoute.ContactsGraph), route("/messages"))
+ assertEquals(listOf(ContactsRoute.Contacts), route("/messages"))
}
// endregion
@@ -96,8 +96,8 @@ class DeepLinkRouterTest {
// region connections
@Test
- fun `connections routes to ConnectionsGraph`() {
- assertEquals(listOf(ConnectionsRoute.ConnectionsGraph), route("/connections"))
+ fun `connections routes to Connections`() {
+ assertEquals(listOf(ConnectionsRoute.Connections), route("/connections"))
}
// endregion
@@ -129,21 +129,21 @@ class DeepLinkRouterTest {
// region nodes
@Test
- fun `nodes root returns NodesGraph`() {
- assertEquals(listOf(NodesRoute.NodesGraph), route("/nodes"))
+ fun `nodes root returns Nodes`() {
+ assertEquals(listOf(NodesRoute.Nodes), route("/nodes"))
}
@Test
fun `nodes with destNum returns NodeDetail`() {
- assertEquals(listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum = 1234)), route("/nodes/1234"))
+ assertEquals(listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 1234)), route("/nodes/1234"))
}
@Test
fun `nodes with destNum and device-metrics sub-route`() {
assertEquals(
listOf(
- NodesRoute.NodesGraph,
- NodesRoute.NodeDetailGraph(destNum = 1234),
+ NodesRoute.Nodes,
+ NodesRoute.NodeDetail(destNum = 1234),
NodeDetailRoute.DeviceMetrics(destNum = 1234),
),
route("/nodes/1234/device-metrics"),
@@ -154,8 +154,8 @@ class DeepLinkRouterTest {
fun `nodes with destNum and map sub-route`() {
assertEquals(
listOf(
- NodesRoute.NodesGraph,
- NodesRoute.NodeDetailGraph(destNum = 5678),
+ NodesRoute.Nodes,
+ NodesRoute.NodeDetail(destNum = 5678),
NodeDetailRoute.PositionLog(destNum = 5678),
),
route("/nodes/5678/map"),
@@ -165,11 +165,7 @@ class DeepLinkRouterTest {
@Test
fun `nodes with destNum and position sub-route`() {
assertEquals(
- listOf(
- NodesRoute.NodesGraph,
- NodesRoute.NodeDetailGraph(destNum = 100),
- NodeDetailRoute.PositionLog(destNum = 100),
- ),
+ listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 100), NodeDetailRoute.PositionLog(destNum = 100)),
route("/nodes/100/position"),
)
}
@@ -178,8 +174,8 @@ class DeepLinkRouterTest {
fun `nodes with destNum and environment sub-route`() {
assertEquals(
listOf(
- NodesRoute.NodesGraph,
- NodesRoute.NodeDetailGraph(destNum = 100),
+ NodesRoute.Nodes,
+ NodesRoute.NodeDetail(destNum = 100),
NodeDetailRoute.EnvironmentMetrics(destNum = 100),
),
route("/nodes/100/environment"),
@@ -190,8 +186,8 @@ class DeepLinkRouterTest {
fun `nodes with destNum and signal sub-route`() {
assertEquals(
listOf(
- NodesRoute.NodesGraph,
- NodesRoute.NodeDetailGraph(destNum = 100),
+ NodesRoute.Nodes,
+ NodesRoute.NodeDetail(destNum = 100),
NodeDetailRoute.SignalMetrics(destNum = 100),
),
route("/nodes/100/signal"),
@@ -201,11 +197,7 @@ class DeepLinkRouterTest {
@Test
fun `nodes with destNum and power sub-route`() {
assertEquals(
- listOf(
- NodesRoute.NodesGraph,
- NodesRoute.NodeDetailGraph(destNum = 100),
- NodeDetailRoute.PowerMetrics(destNum = 100),
- ),
+ listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 100), NodeDetailRoute.PowerMetrics(destNum = 100)),
route("/nodes/100/power"),
)
}
@@ -214,8 +206,8 @@ class DeepLinkRouterTest {
fun `nodes with destNum and traceroute sub-route`() {
assertEquals(
listOf(
- NodesRoute.NodesGraph,
- NodesRoute.NodeDetailGraph(destNum = 100),
+ NodesRoute.Nodes,
+ NodesRoute.NodeDetail(destNum = 100),
NodeDetailRoute.TracerouteLog(destNum = 100),
),
route("/nodes/100/traceroute"),
@@ -226,8 +218,8 @@ class DeepLinkRouterTest {
fun `nodes with destNum and host-metrics sub-route`() {
assertEquals(
listOf(
- NodesRoute.NodesGraph,
- NodesRoute.NodeDetailGraph(destNum = 100),
+ NodesRoute.Nodes,
+ NodesRoute.NodeDetail(destNum = 100),
NodeDetailRoute.HostMetricsLog(destNum = 100),
),
route("/nodes/100/host-metrics"),
@@ -237,11 +229,7 @@ class DeepLinkRouterTest {
@Test
fun `nodes with destNum and pax sub-route`() {
assertEquals(
- listOf(
- NodesRoute.NodesGraph,
- NodesRoute.NodeDetailGraph(destNum = 100),
- NodeDetailRoute.PaxMetrics(destNum = 100),
- ),
+ listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 100), NodeDetailRoute.PaxMetrics(destNum = 100)),
route("/nodes/100/pax"),
)
}
@@ -250,8 +238,8 @@ class DeepLinkRouterTest {
fun `nodes with destNum and neighbors sub-route`() {
assertEquals(
listOf(
- NodesRoute.NodesGraph,
- NodesRoute.NodeDetailGraph(destNum = 100),
+ NodesRoute.Nodes,
+ NodesRoute.NodeDetail(destNum = 100),
NodeDetailRoute.NeighborInfoLog(destNum = 100),
),
route("/nodes/100/neighbors"),
@@ -260,20 +248,17 @@ class DeepLinkRouterTest {
@Test
fun `nodes with destNum and unknown sub-route falls back to NodeDetail`() {
- assertEquals(
- listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum = 1234)),
- route("/nodes/1234/unknown-sub"),
- )
+ assertEquals(listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 1234)), route("/nodes/1234/unknown-sub"))
}
@Test
- fun `nodes with non-numeric destNum returns NodesGraph only`() {
- assertEquals(listOf(NodesRoute.NodesGraph), route("/nodes/not-a-number"))
+ fun `nodes with non-numeric destNum returns Nodes only`() {
+ assertEquals(listOf(NodesRoute.Nodes), route("/nodes/not-a-number"))
}
@Test
fun `nodes with destNum query param`() {
- assertEquals(listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum = 9999)), route("/nodes?destNum=9999"))
+ assertEquals(listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 9999)), route("/nodes?destNum=9999"))
}
// endregion
@@ -281,31 +266,28 @@ class DeepLinkRouterTest {
// region settings
@Test
- fun `settings root returns SettingsGraph`() {
- assertEquals(listOf(SettingsRoute.SettingsGraph(destNum = null)), route("/settings"))
+ fun `settings root returns Settings`() {
+ assertEquals(listOf(SettingsRoute.Settings(destNum = null)), route("/settings"))
}
@Test
fun `settings with destNum`() {
- assertEquals(listOf(SettingsRoute.SettingsGraph(destNum = 1234)), route("/settings/1234"))
+ assertEquals(listOf(SettingsRoute.Settings(destNum = 1234)), route("/settings/1234"))
}
@Test
fun `settings with destNum and sub-route`() {
- assertEquals(
- listOf(SettingsRoute.SettingsGraph(destNum = 1234), SettingsRoute.About),
- route("/settings/1234/about"),
- )
+ assertEquals(listOf(SettingsRoute.Settings(destNum = 1234), SettingsRoute.About), route("/settings/1234/about"))
}
@Test
fun `settings with sub-route without destNum`() {
- assertEquals(listOf(SettingsRoute.SettingsGraph(destNum = null), SettingsRoute.LoRa), route("/settings/lora"))
+ assertEquals(listOf(SettingsRoute.Settings(destNum = null), SettingsRoute.LoRa), route("/settings/lora"))
}
@Test
- fun `settings with unknown sub-route returns SettingsGraph only`() {
- assertEquals(listOf(SettingsRoute.SettingsGraph(destNum = null)), route("/settings/nonexistent-page"))
+ fun `settings with unknown sub-route returns Settings only`() {
+ assertEquals(listOf(SettingsRoute.Settings(destNum = null)), route("/settings/nonexistent-page"))
}
@Test
@@ -349,7 +331,7 @@ class DeepLinkRouterTest {
expectedSubRoutes.forEach { (slug, expectedRoute) ->
assertEquals(
- listOf(SettingsRoute.SettingsGraph(destNum = null), expectedRoute),
+ listOf(SettingsRoute.Settings(destNum = null), expectedRoute),
route("/settings/$slug"),
"Settings sub-route '$slug' did not resolve to $expectedRoute",
)
@@ -361,8 +343,8 @@ class DeepLinkRouterTest {
// region channels
@Test
- fun `channels routes to ChannelsGraph`() {
- assertEquals(listOf(ChannelsRoute.ChannelsGraph), route("/channels"))
+ fun `channels routes to Channels`() {
+ assertEquals(listOf(ChannelsRoute.Channels), route("/channels"))
}
// endregion
@@ -402,8 +384,8 @@ class DeepLinkRouterTest {
@Test
fun `route segments are case insensitive`() {
- assertEquals(listOf(NodesRoute.NodesGraph), route("/Nodes"))
- assertEquals(listOf(ConnectionsRoute.ConnectionsGraph), route("/CONNECTIONS"))
+ assertEquals(listOf(NodesRoute.Nodes), route("/Nodes"))
+ assertEquals(listOf(ConnectionsRoute.Connections), route("/CONNECTIONS"))
}
// endregion

diff --git a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/MultiBackstackTest.kt b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/MultiBackstackTest.kt
index c36375356f..4ec1f71395 100644
--- a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/MultiBackstackTest.kt
+++ b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/MultiBackstackTest.kt
@@ -16,6 +16,7 @@
*/
package org.meshtastic.core.navigation
+import androidx.compose.runtime.mutableStateOf
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import kotlin.test.Test
@@ -23,10 +24,15 @@ import kotlin.test.assertEquals
class MultiBackstackTest {
+ private fun createMultiBackstack(startTab: NavKey): MultiBackstack {
+ val tabRoute = TopLevelDestination.fromNavKey(startTab)?.route ?: startTab
+ return MultiBackstack(startTab, mutableStateOf(tabRoute))
+ }
+
@Test
fun `navigateTopLevel to different tab preserves previous tab stack and activates new tab stack`() {
val startTab = TopLevelDestination.Nodes.route
- val multiBackstack = MultiBackstack(startTab)
+ val multiBackstack = createMultiBackstack(startTab)
val nodesStack =
NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route, NodesRoute.Nodes)) }
@@ -48,7 +54,7 @@ class MultiBackstackTest {
@Test
fun `navigateTopLevel to same tab resets stack to root`() {
val startTab = TopLevelDestination.Nodes.route
- val multiBackstack = MultiBackstack(startTab)
+ val multiBackstack = createMultiBackstack(startTab)
val nodesStack =
NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route, NodesRoute.Nodes)) }
@@ -65,7 +71,7 @@ class MultiBackstackTest {
@Test
fun `goBack pops current stack if size is greater than 1`() {
val startTab = TopLevelDestination.Nodes.route
- val multiBackstack = MultiBackstack(startTab)
+ val multiBackstack = createMultiBackstack(startTab)
val nodesStack =
NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route, NodesRoute.Nodes)) }
@@ -80,7 +86,7 @@ class MultiBackstackTest {
@Test
fun `goBack on root of non-start tab returns to start tab`() {
val startTab = TopLevelDestination.Connections.route
- val multiBackstack = MultiBackstack(startTab)
+ val multiBackstack = createMultiBackstack(startTab)
val mapStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Map.route)) }
val connectionsStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Connections.route)) }
@@ -99,7 +105,7 @@ class MultiBackstackTest {
@Test
fun `handleDeepLink sets target tab and populates stack`() {
val startTab = TopLevelDestination.Nodes.route
- val multiBackstack = MultiBackstack(startTab)
+ val multiBackstack = createMultiBackstack(startTab)
val settingsStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Settings.route)) }
multiBackstack.backStacks = mapOf(TopLevelDestination.Settings.route to settingsStack)
@@ -116,7 +122,7 @@ class MultiBackstackTest {
fun `handleDeepLink from different tab switches tab and sets stack`() {
// Start on Connections tab
val startTab = TopLevelDestination.Connections.route
- val multiBackstack = MultiBackstack(startTab)
+ val multiBackstack = createMultiBackstack(startTab)
val connectionsStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Connections.route)) }
val nodesStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route)) }
@@ -133,13 +139,13 @@ class MultiBackstackTest {
// Deep-link to a TracerouteMap on the Nodes tab (this is the exact pattern
// MeshtasticAppShell uses for traceroute alert "View on Map")
val tracerouteMap = NodeDetailRoute.TracerouteMap(destNum = 100, requestId = 42, logUuid = "abc")
- multiBackstack.handleDeepLink(listOf(NodesRoute.NodesGraph, tracerouteMap))
+ multiBackstack.handleDeepLink(listOf(NodesRoute.Nodes, tracerouteMap))
// Should have switched to the Nodes tab
assertEquals(TopLevelDestination.Nodes.route, multiBackstack.currentTabRoute)
// Stack should contain the graph root + the traceroute map route
assertEquals(2, multiBackstack.activeBackStack.size)
- assertEquals(NodesRoute.NodesGraph, multiBackstack.activeBackStack.first())
+ assertEquals(NodesRoute.Nodes, multiBackstack.activeBackStack.first())
assertEquals(tracerouteMap, multiBackstack.activeBackStack.last())
}
}

diff --git a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavBackStackExtTest.kt b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavBackStackExtTest.kt
index 2f013a39c6..8ffb917e61 100644
--- a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavBackStackExtTest.kt
+++ b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavBackStackExtTest.kt
@@ -26,21 +26,21 @@ class NavBackStackExtTest {
@Test
fun `replaceLast on non-empty list replaces the last element`() {
- val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes)
+ val stack = mutableListOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes)
stack.replaceLast(NodesRoute.NodeDetail(destNum = 42))
assertEquals(2, stack.size)
- assertEquals(NodesRoute.NodesGraph, stack[0])
+ assertEquals(NodesRoute.Nodes, stack[0])
assertEquals(NodesRoute.NodeDetail(destNum = 42), stack[1])
}
@Test
fun `replaceLast on single-element list replaces that element`() {
- val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph)
- stack.replaceLast(SettingsRoute.SettingsGraph())
+ val stack = mutableListOf<NavKey>(NodesRoute.Nodes)
+ stack.replaceLast(SettingsRoute.Settings())
assertEquals(1, stack.size)
- assertEquals(SettingsRoute.SettingsGraph(), stack[0])
+ assertEquals(SettingsRoute.Settings(), stack[0])
}
@Test
@@ -55,7 +55,7 @@ class NavBackStackExtTest {
@Test
fun `replaceLast with same element does not mutate`() {
val route = NodesRoute.Nodes
- val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, route)
+ val stack = mutableListOf<NavKey>(NodesRoute.Nodes, route)
stack.replaceLast(route)
assertEquals(2, stack.size)
@@ -68,8 +68,8 @@ class NavBackStackExtTest {
@Test
fun `replaceAll replaces entire stack with new routes`() {
- val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes)
- val newRoutes = listOf<NavKey>(SettingsRoute.SettingsGraph(), SettingsRoute.About)
+ val stack = mutableListOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes)
+ val newRoutes = listOf<NavKey>(SettingsRoute.Settings(), SettingsRoute.About)
stack.replaceAll(newRoutes)
@@ -78,19 +78,19 @@ class NavBackStackExtTest {
@Test
fun `replaceAll with shorter list trims excess elements`() {
- val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 42))
- val newRoutes = listOf<NavKey>(SettingsRoute.SettingsGraph())
+ val stack = mutableListOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 42))
+ val newRoutes = listOf<NavKey>(SettingsRoute.Settings())
stack.replaceAll(newRoutes)
assertEquals(1, stack.size)
- assertEquals(SettingsRoute.SettingsGraph(), stack[0])
+ assertEquals(SettingsRoute.Settings(), stack[0])
}
@Test
fun `replaceAll with longer list appends new elements`() {
- val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph)
- val newRoutes = listOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 99))
+ val stack = mutableListOf<NavKey>(NodesRoute.Nodes)
+ val newRoutes = listOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 99))
stack.replaceAll(newRoutes)
@@ -99,7 +99,7 @@ class NavBackStackExtTest {
@Test
fun `replaceAll with empty list clears the stack`() {
- val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes)
+ val stack = mutableListOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes)
stack.replaceAll(emptyList())
@@ -109,7 +109,7 @@ class NavBackStackExtTest {
@Test
fun `replaceAll on empty stack with new routes populates it`() {
val stack = mutableListOf<NavKey>()
- val newRoutes = listOf<NavKey>(ContactsRoute.ContactsGraph, ContactsRoute.Contacts)
+ val newRoutes = listOf<NavKey>(ContactsRoute.Contacts, ContactsRoute.Contacts)
stack.replaceAll(newRoutes)
@@ -118,7 +118,7 @@ class NavBackStackExtTest {
@Test
fun `replaceAll with identical routes does not mutate entries`() {
- val routes = listOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes)
+ val routes = listOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes)
val stack = routes.toMutableList()
stack.replaceAll(routes)
@@ -128,17 +128,17 @@ class NavBackStackExtTest {
@Test
fun `replaceAll with partial overlap only changes differing elements`() {
- val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 1))
+ val stack = mutableListOf<NavKey>(NodesRoute.Nodes, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 1))
val newRoutes =
listOf<NavKey>(
- NodesRoute.NodesGraph, // same
+ NodesRoute.Nodes, // same
SettingsRoute.About, // different
)
stack.replaceAll(newRoutes)
assertEquals(2, stack.size)
- assertEquals(NodesRoute.NodesGraph, stack[0])
+ assertEquals(NodesRoute.Nodes, stack[0])
assertEquals(SettingsRoute.About, stack[1])
}

diff --git a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavigationConfigTest.kt b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavigationConfigTest.kt
index 293c567fc7..2f7ae59af1 100644
--- a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavigationConfigTest.kt
+++ b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavigationConfigTest.kt
@@ -38,13 +38,10 @@ class NavigationConfigTest {
private val allRouteInstances: List<NavKey> =
listOf(
// ChannelsRoute
- ChannelsRoute.ChannelsGraph,
ChannelsRoute.Channels,
// ConnectionsRoute
- ConnectionsRoute.ConnectionsGraph,
ConnectionsRoute.Connections,
// ContactsRoute
- ContactsRoute.ContactsGraph,
ContactsRoute.Contacts,
ContactsRoute.Messages(contactKey = "test-contact", message = "hello"),
ContactsRoute.Messages(contactKey = "test-contact"),
@@ -54,10 +51,7 @@ class NavigationConfigTest {
MapRoute.Map(),
MapRoute.Map(waypointId = 42),
// NodesRoute
- NodesRoute.NodesGraph,
NodesRoute.Nodes,
- NodesRoute.NodeDetailGraph(destNum = 1234),
- NodesRoute.NodeDetailGraph(),
NodesRoute.NodeDetail(destNum = 5678),
NodesRoute.NodeDetail(),
// NodeDetailRoute
@@ -73,8 +67,6 @@ class NavigationConfigTest {
NodeDetailRoute.PaxMetrics(destNum = 100),
NodeDetailRoute.NeighborInfoLog(destNum = 100),
// SettingsRoute
- SettingsRoute.SettingsGraph(),
- SettingsRoute.SettingsGraph(destNum = 999),
SettingsRoute.Settings(),
SettingsRoute.Settings(destNum = 999),
SettingsRoute.DeviceConfiguration,
@@ -183,9 +175,7 @@ class NavigationConfigTest {
val routesWithDefaults: List<Pair<NavKey, NavKey>> =
listOf(
MapRoute.Map() to MapRoute.Map(waypointId = null),
- NodesRoute.NodeDetailGraph() to NodesRoute.NodeDetailGraph(destNum = null),
NodesRoute.NodeDetail() to NodesRoute.NodeDetail(destNum = null),
- SettingsRoute.SettingsGraph() to SettingsRoute.SettingsGraph(destNum = null),
SettingsRoute.Settings() to SettingsRoute.Settings(destNum = null),
WifiProvisionRoute.WifiProvision() to WifiProvisionRoute.WifiProvision(address = null),
)

diff --git a/core/ui/detekt-baseline.xml b/core/ui/detekt-baseline.xml
index ab18a483ff..11672934f8 100644
--- a/core/ui/detekt-baseline.xml
+++ b/core/ui/detekt-baseline.xml
@@ -127,7 +127,7 @@
<ID>PreviewPublic:SwitchPreference.kt:@Preview(showBackground = true) @Composable fun SwitchPreferencePreview</ID>
<ID>PreviewPublic:TextDividerPreference.kt:@Preview(showBackground = true) @Composable fun TextDividerPreferencePreview</ID>
<ID>PreviewPublic:TitledCard.kt:@PreviewLightDark @Composable fun TitledCardPreview</ID>
- <ID>ViewModelForwarding:MeshtasticAppShell.kt:MeshtasticCommonAppSetup( uiViewModel = uiViewModel, onNavigateToTracerouteMap = { destNum, requestId, logUuid -> multiBackstack.handleDeepLink( listOf( NodesRoute.NodesGraph, NodeDetailRoute.TracerouteMap(destNum = destNum, requestId = requestId, logUuid = logUuid), ), ) }, )</ID>
+ <ID>ViewModelForwarding:MeshtasticAppShell.kt:MeshtasticCommonAppSetup( uiViewModel = uiViewModel, onNavigateToTracerouteMap = { destNum, requestId, logUuid -> multiBackstack.handleDeepLink( listOf( NodesRoute.Nodes, NodeDetailRoute.TracerouteMap(destNum = destNum, requestId = requestId, logUuid = logUuid), ), ) }, )</ID>
<ID>ViewModelForwarding:MeshtasticCommonAppSetup.kt:FirmwareVersionCheck(viewModel = uiViewModel)</ID>
<ID>ViewModelForwarding:MeshtasticCommonAppSetup.kt:SharedDialogs(uiViewModel = uiViewModel)</ID>
<ID>ViewModelForwarding:MeshtasticCommonAppSetup.kt:TracerouteAlertHandler(uiViewModel = uiViewModel, onNavigateToMap = onNavigateToTracerouteMap)</ID>

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticAppShell.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticAppShell.kt
index 95a64ff95c..9f6077e2d6 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticAppShell.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticAppShell.kt
@@ -46,7 +46,7 @@ fun MeshtasticAppShell(
onNavigateToTracerouteMap = { destNum, requestId, logUuid ->
multiBackstack.handleDeepLink(
listOf(
- NodesRoute.NodesGraph,
+ NodesRoute.Nodes,
NodeDetailRoute.TracerouteMap(destNum = destNum, requestId = requestId, logUuid = logUuid),
),
)

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt
index 08d63c605b..c05cb71be0 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt
@@ -18,6 +18,8 @@ package org.meshtastic.core.ui.component
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -68,7 +70,7 @@ fun MeshtasticNavDisplay(
/** Shared [NavDisplay] wrapper for a single backstack. */
@Suppress("LongMethod")
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalSharedTransitionApi::class)
@Composable
fun MeshtasticNavDisplay(
backStack: NavBackStack<NavKey>,
@@ -115,28 +117,32 @@ fun MeshtasticNavDisplay(
val activeDecorators =
remember(backStack, saveableDecorator, vmStoreDecorator) { listOf(saveableDecorator, vmStoreDecorator) }
- NavDisplay(
- backStack = backStack,
- entryProvider = entryProvider,
- entryDecorators = activeDecorators,
- onBack =
- onBack
- ?: {
- if (backStack.size > 1) {
- backStack.removeLastOrNull()
- }
- },
- sceneStrategies =
- listOf(
- DialogSceneStrategy(),
- listDetailSceneStrategy,
- supportingPaneSceneStrategy,
- SinglePaneSceneStrategy(),
- ),
- transitionSpec = meshtasticTransitionSpec(),
- popTransitionSpec = meshtasticTransitionSpec(),
- modifier = modifier,
- )
+ SharedTransitionLayout {
+ NavDisplay(
+ backStack = backStack,
+ entryProvider = entryProvider,
+ entryDecorators = activeDecorators,
+ onBack =
+ onBack
+ ?: {
+ if (backStack.size > 1) {
+ backStack.removeLastOrNull()
+ }
+ },
+ sceneStrategies =
+ listOf(
+ DialogSceneStrategy(),
+ listDetailSceneStrategy,
+ supportingPaneSceneStrategy,
+ SinglePaneSceneStrategy(),
+ ),
+ sharedTransitionScope = this@SharedTransitionLayout,
+ transitionSpec = meshtasticTransitionSpec(),
+ popTransitionSpec = meshtasticTransitionSpec(),
+ predictivePopTransitionSpec = meshtasticPredictivePopTransitionSpec(),
+ modifier = modifier,
+ )
+ }
}
/** Shared crossfade [ContentTransform] used for both forward and pop navigation. */
@@ -146,3 +152,13 @@ private fun meshtasticTransitionSpec(): AnimatedContentTransitionScope<Scene<Nav
fadeOut(animationSpec = tween(TRANSITION_DURATION_MS)),
)
}
+
+/** Crossfade transition for predictive back gestures (Android 14+). */
+private fun meshtasticPredictivePopTransitionSpec():
+ AnimatedContentTransitionScope<Scene<NavKey>>.(Int) -> ContentTransform =
+ {
+ ContentTransform(
+ fadeIn(animationSpec = tween(TRANSITION_DURATION_MS)),
+ fadeOut(animationSpec = tween(TRANSITION_DURATION_MS)),
+ )
+ }

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt
index f23f082b5c..de477fc4a5 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt
@@ -141,7 +141,7 @@ private fun handleNavigation(
val currentKey = multiBackstack.activeBackStack.lastOrNull()
when (destination) {
TopLevelDestination.Nodes -> {
- val onNodesList = currentKey is NodesRoute.NodesGraph || currentKey is NodesRoute.Nodes
+ val onNodesList = currentKey is NodesRoute.Nodes
if (!onNodesList) {
multiBackstack.navigateTopLevel(destination.route)
} else {
@@ -150,8 +150,7 @@ private fun handleNavigation(
}
TopLevelDestination.Conversations -> {
- val onConversationsList =
- currentKey is ContactsRoute.ContactsGraph || currentKey is ContactsRoute.Contacts
+ val onConversationsList = currentKey is ContactsRoute.Contacts
if (!onConversationsList) {
multiBackstack.navigateTopLevel(destination.route)
} else {

diff --git a/desktopApp/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt
index b77d6a2226..ac81053ef1 100644
--- a/desktopApp/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt
+++ b/desktopApp/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt
@@ -41,11 +41,11 @@ class DesktopTopLevelDestinationParityTest {
val androidParityRoutes: Set<KClass<out Route>> =
setOf(
- ContactsRoute.ContactsGraph::class,
- NodesRoute.NodesGraph::class,
+ ContactsRoute.Contacts::class,
+ NodesRoute.Nodes::class,
MapRoute.Map::class,
- SettingsRoute.SettingsGraph::class,
- ConnectionsRoute.ConnectionsGraph::class,
+ SettingsRoute.Settings::class,
+ ConnectionsRoute.Connections::class,
)
assertEquals(

diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/navigation/ConnectionsNavigation.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/navigation/ConnectionsNavigation.kt
index 7b91f09754..23fa20d591 100644
--- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/navigation/ConnectionsNavigation.kt
+++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/navigation/ConnectionsNavigation.kt
@@ -28,16 +28,6 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
/** Navigation graph for for the top level ConnectionsScreen - [ConnectionsRoute.Connections]. */
fun EntryProviderScope<NavKey>.connectionsGraph(backStack: NavBackStack<NavKey>) {
- entry<ConnectionsRoute.ConnectionsGraph> {
- ConnectionsScreen(
- scanModel = koinViewModel<ScannerViewModel>(),
- radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),
- onClickNodeChip = { id -> backStack.add(NodesRoute.NodeDetail(id)) },
- onNavigateToNodeDetails = { id -> backStack.add(NodesRoute.NodeDetail(id)) },
- onConfigNavigate = { route -> backStack.add(route) },
- )
- }
-
entry<ConnectionsRoute.Connections> {
ConnectionsScreen(
scanModel = koinViewModel<ScannerViewModel>(),

diff --git a/feature/messaging/detekt-baseline.xml b/feature/messaging/detekt-baseline.xml
index 2986c92922..6363e93755 100644
--- a/feature/messaging/detekt-baseline.xml
+++ b/feature/messaging/detekt-baseline.xml
@@ -35,6 +35,6 @@
<ID>PreviewPublic:QuickChatPreviews.kt:@PreviewLightDark @Composable fun EditQuickChatDialogPreview</ID>
<ID>PreviewPublic:QuickChatPreviews.kt:@PreviewLightDark @Composable fun QuickChatItemPreview</ID>
<ID>PreviewPublic:ReactionPreviews.kt:@PreviewLightDark @Composable fun ReactionItemPreview</ID>
- <ID>ViewModelForwarding:AdaptiveContactsScreen.kt:ContactsScreen( onNavigateToShare = { backStack.add(ChannelsRoute.ChannelsGraph) }, sharedContactRequested = sharedContactRequested, requestChannelSet = requestChannelSet, onHandleDeepLink = onHandleDeepLink, onClearSharedContactRequested = onClearSharedContactRequested, onClearRequestChannelUrl = onClearRequestChannelUrl, viewModel = contactsViewModel, onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) }, onNavigateToMessages = { contactKey -> backStack.add(ContactsRoute.Messages(contactKey)) }, onNavigateToNodeDetails = { backStack.add(NodesRoute.NodeDetail(it)) }, scrollToTopEvents = scrollToTopEvents, activeContactKey = null, )</ID>
+ <ID>ViewModelForwarding:AdaptiveContactsScreen.kt:ContactsScreen( onNavigateToShare = { backStack.add(ChannelsRoute.Channels) }, sharedContactRequested = sharedContactRequested, requestChannelSet = requestChannelSet, onHandleDeepLink = onHandleDeepLink, onClearSharedContactRequested = onClearSharedContactRequested, onClearRequestChannelUrl = onClearRequestChannelUrl, viewModel = contactsViewModel, onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) }, onNavigateToMessages = { contactKey -> backStack.add(ContactsRoute.Messages(contactKey)) }, onNavigateToNodeDetails = { backStack.add(NodesRoute.NodeDetail(it)) }, scrollToTopEvents = scrollToTopEvents, activeContactKey = null, )</ID>
</CurrentIssues>
</SmellBaseline>

diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt
index f6c27c3c67..899ae73441 100644
--- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt
+++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt
@@ -45,10 +45,6 @@ fun EntryProviderScope<NavKey>.contactsGraph(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent> = MutableSharedFlow(),
) {
- entry<ContactsRoute.ContactsGraph>(metadata = { ListDetailSceneStrategy.listPane() }) {
- ContactsEntryContent(backStack = backStack, scrollToTopEvents = scrollToTopEvents)
- }
-
entry<ContactsRoute.Contacts>(metadata = { ListDetailSceneStrategy.listPane() }) {
ContactsEntryContent(backStack = backStack, scrollToTopEvents = scrollToTopEvents)
}

diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt
index c87b9143f0..278ad2f455 100644
--- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt
+++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt
@@ -40,7 +40,7 @@ fun AdaptiveContactsScreen(
onClearRequestChannelUrl: () -> Unit,
) {
ContactsScreen(
- onNavigateToShare = { backStack.add(ChannelsRoute.ChannelsGraph) },
+ onNavigateToShare = { backStack.add(ChannelsRoute.Channels) },
sharedContactRequested = sharedContactRequested,
requestChannelSet = requestChannelSet,
onHandleDeepLink = onHandleDeepLink,

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/AdaptiveNodeListScreen.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/AdaptiveNodeListScreen.kt
index f10f456ac4..882e078f33 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/AdaptiveNodeListScreen.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/AdaptiveNodeListScreen.kt
@@ -39,7 +39,7 @@ fun AdaptiveNodeListScreen(
NodeListScreen(
viewModel = nodeListViewModel,
navigateToNodeDetails = { nodeId -> backStack.add(NodesRoute.NodeDetail(nodeId)) },
- onNavigateToChannels = { backStack.add(ChannelsRoute.ChannelsGraph) },
+ onNavigateToChannels = { backStack.add(ChannelsRoute.Channels) },
scrollToTopEvents = scrollToTopEvents,
activeNodeId = null,
onHandleDeepLink = onHandleDeepLink,

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt
index 47d34c9c5b..ee4955c321 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt
@@ -76,15 +76,6 @@ fun EntryProviderScope<NavKey>.nodesGraph(
onHandleDeepLink: (org.meshtastic.core.common.util.CommonUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
onNavigateToConnections: () -> Unit = {},
) {
- entry<NodesRoute.NodesGraph>(metadata = { ListDetailSceneStrategy.listPane() }) {
- AdaptiveNodeListScreen(
- backStack = backStack,
- scrollToTopEvents = scrollToTopEvents,
- onHandleDeepLink = onHandleDeepLink,
- onNavigateToConnections = onNavigateToConnections,
- )
- }
-
entry<NodesRoute.Nodes>(metadata = { ListDetailSceneStrategy.listPane() }) {
AdaptiveNodeListScreen(
backStack = backStack,
@@ -94,30 +85,16 @@ fun EntryProviderScope<NavKey>.nodesGraph(
)
}
- nodeDetailGraph(backStack, scrollToTopEvents, onHandleDeepLink, onNavigateToConnections)
+ nodeDetailGraph(backStack)
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Suppress("LongMethod")
-fun EntryProviderScope<NavKey>.nodeDetailGraph(
- backStack: NavBackStack<NavKey>,
- scrollToTopEvents: Flow<ScrollToTopEvent>,
- onHandleDeepLink: (org.meshtastic.core.common.util.CommonUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
- onNavigateToConnections: () -> Unit = {},
-) {
- entry<NodesRoute.NodeDetailGraph>(metadata = { ListDetailSceneStrategy.listPane() }) { args ->
- AdaptiveNodeListScreen(
- backStack = backStack,
- scrollToTopEvents = scrollToTopEvents,
- onHandleDeepLink = onHandleDeepLink,
- onNavigateToConnections = onNavigateToConnections,
- )
- }
-
+fun EntryProviderScope<NavKey>.nodeDetailGraph(backStack: NavBackStack<NavKey>) {
entry<NodesRoute.NodeDetail>(metadata = { ListDetailSceneStrategy.detailPane() }) { args ->
val nodeDetailViewModel: NodeDetailViewModel = koinViewModel()
val compassViewModel: CompassViewModel = koinViewModel()
- val destNum = args.destNum ?: 0 // Handle nullable destNum if needed
+ val destNum = args.destNum ?: 0
NodeDetailScreen(
nodeId = destNum,
viewModel = nodeDetailViewModel,
@@ -149,7 +126,12 @@ fun EntryProviderScope<NavKey>.nodeDetailGraph(
entry<NodeDetailRoute.TracerouteMap>(metadata = { ListDetailSceneStrategy.extraPane() }) { args ->
val tracerouteMapScreen = org.meshtastic.core.ui.util.LocalTracerouteMapScreenProvider.current
- tracerouteMapScreen(args.destNum, args.requestId, args.logUuid) { backStack.removeLastOrNull() }
+ tracerouteMapScreen(
+ args.destNum,
+ args.requestId,
+ args.logUuid,
+ dropUnlessResumed { backStack.removeLastOrNull() },
+ )
}
NodeDetailScreen.entries.forEach { routeInfo ->

diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt
index d0e4f4a009..18e2707bba 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt
@@ -73,14 +73,12 @@ import org.meshtastic.feature.settings.radio.component.UserConfigScreen
import kotlin.reflect.KClass
@Composable
-fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>): RadioConfigViewModel {
+fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>, destNumOverride: Int? = null): RadioConfigViewModel {
val destNum =
- remember(backStack.toList()) {
- backStack.lastOrNull { it is SettingsRoute.Settings }?.let { (it as SettingsRoute.Settings).destNum }
- ?: backStack
- .lastOrNull { it is SettingsRoute.SettingsGraph }
- ?.let { (it as SettingsRoute.SettingsGraph).destNum }
- }
+ destNumOverride
+ ?: remember(backStack.toList()) {
+ backStack.lastOrNull { it is SettingsRoute.Settings }?.let { (it as SettingsRoute.Settings).destNum }
+ }
return koinViewModel<RadioConfigViewModel>(key = destNum?.toString()) {
parametersOf(SavedStateHandle(mapOf("destNum" to destNum)))
}
@@ -88,22 +86,14 @@ fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>): RadioConfigViewMod
@Suppress("LongMethod", "CyclomaticComplexMethod")
fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
- entry<SettingsRoute.SettingsGraph> {
- SettingsMainScreen(
- settingsViewModel = koinViewModel(),
- radioConfigViewModel = getRadioConfigViewModel(backStack),
- onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) },
- onNavigate = { backStack.add(it) },
- )
- }
-
- entry<SettingsRoute.Settings> {
+ entry<SettingsRoute.Settings> { args ->
+ val isTabRoot = backStack.firstOrNull() == args
SettingsMainScreen(
settingsViewModel = koinViewModel(),
- radioConfigViewModel = getRadioConfigViewModel(backStack),
+ radioConfigViewModel = getRadioConfigViewModel(backStack, destNumOverride = args.destNum),
onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) },
onNavigate = { backStack.add(it) },
- onBack = dropUnlessResumed { backStack.removeLastOrNull() },
+ onBack = if (isTabRoot) null else dropUnlessResumed { backStack.removeLastOrNull() },
)
}
@@ -236,7 +226,7 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
}
}
- entry<SettingsRoute.TakServer> { TakServerScreen(onBack = { backStack.removeLastOrNull() }) }
+ entry<SettingsRoute.TakServer> { TakServerScreen(onBack = dropUnlessResumed { backStack.removeLastOrNull() }) }
entry<SettingsRoute.DebugPanel> {
val viewModel: DebugViewModel = koinViewModel()

diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelsNavigation.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelsNavigation.kt
index 428b6602cb..6218a0a90b 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelsNavigation.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelsNavigation.kt
@@ -26,14 +26,6 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
/** Navigation graph for for the top level ChannelScreen - [ChannelsRoute.Channels]. */
fun EntryProviderScope<NavKey>.channelsGraph(backStack: NavBackStack<NavKey>) {
- entry<ChannelsRoute.ChannelsGraph> {
- ChannelScreen(
- radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),
- onNavigate = { route -> backStack.add(route) },
- onNavigateUp = dropUnlessResumed { backStack.removeLastOrNull() },
- )
- }
-
entry<ChannelsRoute.Channels> {
ChannelScreen(
radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),

Served by rngit 1.5.4 - Generated in 0.28s